// FSFolder.cpp

#include "StdAfx.h"

#include "Common/String/StringConvert.h"
#include "Common/String/UTFConvert.h"

#include "Common/FileSystem/FileDir.h"
#include "Common/Stream/FileIO.h"
#include "Common/MS/PropVariant.h"

#include "Common/7Zip/PropID.h"

#include "FSDrives.h"
#include "FSFolder.h"

#include "NetFolder.h"

#include "SysIconUtils.h"

#include "Common/Defs.h"

namespace Common
{
namespace FileSystem
{

bool GetLongPath(LPCTSTR path, CString &longPath);

using namespace Find;

namespace Folder
{

static STATPROPSTG kProps[] =
{
	{ NULL, kpidName, VT_BSTR},
	{ NULL, kpidSize, VT_UI8},
	{ NULL, kpidMTime, VT_FILETIME},
	{ NULL, kpidCTime, VT_FILETIME},
	{ NULL, kpidATime, VT_FILETIME},
	{ NULL, kpidAttrib, VT_UI4},
	{ NULL, kpidPackSize, VT_UI8},
	{ NULL, kpidComment, VT_BSTR},
	{ NULL, kpidPrefix, VT_BSTR}
};

HRESULT CFSFolder::Init(const CString &path, IFolderFolder* parentFolder)
{
	_parentFolder = parentFolder;
	_path = path;

	_findChangeNotification.FindFirst(_path, false,
									  FILE_NOTIFY_CHANGE_FILE_NAME |
									  FILE_NOTIFY_CHANGE_DIR_NAME |
									  FILE_NOTIFY_CHANGE_ATTRIBUTES |
									  FILE_NOTIFY_CHANGE_SIZE |
									  FILE_NOTIFY_CHANGE_LAST_WRITE /*|
      FILE_NOTIFY_CHANGE_LAST_ACCESS |
      FILE_NOTIFY_CHANGE_CREATION |
      FILE_NOTIFY_CHANGE_SECURITY */);
	if (!_findChangeNotification.IsHandleAllocated())
	{
		DWORD lastError = GetLastError();
		Common::FileSystem::Find::CFindFile findFile;
		Common::FileSystem::File::CFileInfo fi;
		if (!findFile.FindFirst(_path + CString(_T("*")), &fi))
		{
			return lastError;
		}
	}
	return LoadItems();;
}

HRESULT GetFolderSize(const CString &path, UINT64 &numFolders, UINT64 &numFiles, UINT64 &size, IProgress* progress)
{
	HRESULT hRes(progress->SetCompleted(NULL));
	if(S_OK != hRes)
	{
		return hRes;
	}
	numFiles = numFolders = size = 0;
	CEnumerator enumerator(path + CString(STRING_PATH_SEPARATOR _T("*")));
	File::CFileInfo fi;
	while (enumerator.Next(&fi))
	{
		if (fi.IsDir())
		{
			UINT64 subFolders, subFiles, subSize;
			hRes = GetFolderSize(path + CString(CHAR_PATH_SEPARATOR) + fi.Name, subFolders, subFiles, subSize, progress);
			if(S_OK != hRes)
			{
				return hRes;
			}
			numFolders += subFolders;
			numFolders++;
			numFiles += subFiles;
			size += subSize;
		}
		else
		{
			numFiles++;
			size += fi.Size;
		}
	}
	return S_OK;
}

HRESULT CFSFolder::LoadSubItems(std::shared_ptr<CDirItem> &dirItem, const CString &path)
{

	{
		CEnumerator enumerator(path + _T("*"));
		std::shared_ptr<CDirItem> fi = std::make_shared<CDirItem>() ;
		while (enumerator.Next(fi.get()))
		{
			fi->CompressedSizeIsDefined = false;
			/*
			if (!GetCompressedFileSize(_path + fi.Name,
			fi.CompressedSize))
			fi.CompressedSize = fi.Size;
			*/
			if (fi->IsDir())
			{
				// fi.Size = GetFolderSize(_path + fi.Name);
				fi->Size = 0;
			}
			dirItem->Files.push_back(fi);
			fi = std::make_shared<CDirItem>() ;
		}
	}
	if (!_flatMode)
	{
		return S_OK;
	}

	for (size_t i = 0; i < dirItem->Files.size(); i++)
	{
		auto &item = dirItem->Files[i];
		if (item->IsDir())
		{
			LoadSubItems(item, path + item->Name + CHAR_PATH_SEPARATOR);
		}
	}
	return S_OK;
}

void CFSFolder::AddRefs(CDirItem::PtrDirItem &dirItem)
{
	size_t i;
	for (i = 0; i < dirItem->Files.size(); i++)
	{
		CDirItem::PtrDirItem item = dirItem->Files[i];
		item->Parent = dirItem;
		_refs.push_back(item);
	}
	if (!_flatMode)
	{
		return;
	}
	for (i = 0; i < dirItem->Files.size(); i++)
	{
		CDirItem::PtrDirItem &item = dirItem->Files[i];
		if (item->IsDir())
		{
			AddRefs(item);
		}
	}
}

STDMETHODIMP CFSFolder::LoadItems()
{
	// OutputDebugString(TEXT("Start\n"));
	INT32 dummy;
	WasChanged(&dummy);
	Clear();
	HRESULT hRes(LoadSubItems(_root, _path));
	if(S_OK != hRes)
	{
		return hRes;
	}
	AddRefs(_root);

	// OutputDebugString(TEXT("Finish\n"));
	_commentsAreLoaded = false;
	return S_OK;
}

static const wchar_t* kDescriptionFileName = _T("descript.ion");

bool CFSFolder::LoadComments()
{
	if (_commentsAreLoaded)
	{
		return true;
	}
	_comments.Clear();
	_commentsAreLoaded = true;
	Common::IO::CInFile file;
	if (!file.Open(_path + kDescriptionFileName))
	{
		return false;
	}
	UINT64 length;
	if (!file.GetLength(length))
	{
		return false;
	}
	if (length >= (1 << 28))
	{
		return false;
	}
	CStringA s;
	char* p = s.GetBuffer((int)((size_t)length + 1));
	UINT32 processedSize;
	file.Read(p, (UINT32)length, processedSize);
	p[length] = 0;
	s.ReleaseBuffer();
	if (processedSize != length)
	{
		return false;
	}
	file.Close();
	CString unicodeString;
	if (!String::ConvertUTF8ToUnicode(s, unicodeString))
	{
		return false;
	}
	return _comments.ReadFromString(unicodeString);
}

static bool IsAscii(const CString &testString)
{
	for (int i = 0; i < testString.GetLength(); i++)
		if (testString[i] >= 0x80)
		{
			return false;
		}
	return true;
}

bool CFSFolder::SaveComments()
{
	Common::IO::COutFile file;
	if (!file.Create(_path + kDescriptionFileName, true))
	{
		return false;
	}
	CString unicodeString;
	_comments.SaveToString(unicodeString);
	CStringA utfString;
	String::ConvertUnicodeToUTF8(unicodeString, utfString);
	UINT32 processedSize;
	if (!IsAscii(unicodeString))
	{
		BYTE bom [] = { 0xEF, 0xBB, 0xBF, 0x0D, 0x0A };
		file.Write(bom , sizeof(bom), processedSize);
	}
	file.Write(utfString, utfString.GetLength(), processedSize);
	_commentsAreLoaded = false;
	return true;
}

STDMETHODIMP CFSFolder::GetNumberOfItems(UINT32* numItems)
{
	*numItems = _refs.size();
	return S_OK;
}

/*
STDMETHODIMP CFSFolder::GetNumberOfSubFolders(UINT32 *numSubFolders)
{
  UINT32 numSubFoldersLoc = 0;
  for (int i = 0; i < _files.Size(); i++)
    if (_files[i].IsDir())
      numSubFoldersLoc++;
  *numSubFolders = numSubFoldersLoc;
  return S_OK;
}
*/

static bool MyGetCompressedFileSizeW(LPCWSTR fileName, UINT64 &size)
{
	DWORD highPart;
	DWORD lowPart = ::GetCompressedFileSizeW(fileName, &highPart);
	if (lowPart == INVALID_FILE_SIZE && ::GetLastError() != NO_ERROR)
	{
#ifdef WIN_LONG_PATH
		{
			CString longPath;
			if (GetLongPath(fileName, longPath))
			{
				lowPart = ::GetCompressedFileSizeW(longPath, &highPart);
			}
		}
#endif
		if (lowPart == INVALID_FILE_SIZE && ::GetLastError() != NO_ERROR)
		{
			return false;
		}
	}
	size = (UINT64(highPart) << 32) | lowPart;
	return true;
}

STDMETHODIMP CFSFolder::GetProperty(UINT32 itemIndex, PROPID propID, PROPVARIANT* value)
{
	CComPropVariant prop;
	if (itemIndex >= (UINT32)_refs.size())
	{
		return E_INVALIDARG;
	}
	CDirItem &fi = *_refs[itemIndex];
	switch(propID)
	{
		case kpidIsDir:
			prop = fi.IsDir();
			break;
		case kpidName:
			prop = CComBSTR(fi.Name);
			break;
		case kpidSize:
			if (!fi.IsDir())
			{
				prop = fi.Size;
			}
			break;
		case kpidPackSize:
			if (!fi.CompressedSizeIsDefined)
			{
				fi.CompressedSizeIsDefined = true;
				if (fi.IsDir () ||
						!MyGetCompressedFileSizeW(_path + GetRelPath(fi), fi.CompressedSize))
				{
					fi.CompressedSize = fi.Size;
				}
			}
			prop = fi.CompressedSize;
			break;
		case kpidAttrib:
			prop = (UINT32)fi.Attributes;
			break;
		case kpidCTime:
			prop = fi.CreationTime;
			break;
		case kpidATime:
			prop = fi.AccessTime;
			break;
		case kpidMTime:
			prop = fi.ModificationTime;
			break;
		case kpidComment:
		{
			LoadComments();
			CString comment;
			if (_comments.GetValue(GetRelPath(fi), comment))
			{
				prop = CComBSTR(comment);
			}
			break;
		}
		case kpidPrefix:
		{
			if (_flatMode)
			{
				prop = CComBSTR(GetPrefix(fi));
			}
			break;
		}
	}
	prop.Detach(value);
	return S_OK;
}

HRESULT CFSFolder::BindToFolderSpec(const wchar_t* name, IFolderFolder** resultFolder)
{
	*resultFolder = 0;
	CFSFolder* folderSpec = new CComObject<CFSFolder>;
	folderSpec->FinalConstruct();
	CComPtr<IFolderFolder> subFolder = folderSpec;
	HRESULT hRes(folderSpec->Init(_path + name + CString(CHAR_PATH_SEPARATOR), 0));
	if(S_OK != hRes)
	{
		return hRes;
	}
	*resultFolder = subFolder.Detach();
	return S_OK;
}

CString CFSFolder::GetPrefix(const CDirItem &item) const
{
	CString path;
	const CDirItem::PtrDirItem* cur = &item.Parent;
	while (NULL != (*cur)->Parent.get())
	{
		path = (*cur)->Name + CString(CHAR_PATH_SEPARATOR) + path;
		cur = &(*cur)->Parent;
	}
	return path;
}

CString CFSFolder::GetRelPath(const CDirItem &item) const
{
	return GetPrefix(item) + item.Name;
}

STDMETHODIMP CFSFolder::BindToFolder(UINT32 index, IFolderFolder** resultFolder)
{
	*resultFolder = 0;
	const CDirItem &fi = *_refs[index];
	if (!fi.IsDir())
	{
		return E_INVALIDARG;
	}
	return BindToFolderSpec(GetRelPath(fi), resultFolder);
}

STDMETHODIMP CFSFolder::BindToFolder(const wchar_t* name, IFolderFolder** resultFolder)
{
	return BindToFolderSpec(name, resultFolder);
}

STDMETHODIMP CFSFolder::BindToParentFolder(IFolderFolder** resultFolder)
{
	*resultFolder = 0;
	if (_parentFolder)
	{
		CComPtr<IFolderFolder> parentFolder = _parentFolder;
		*resultFolder = parentFolder.Detach();
		return S_OK;
	}
	if (_path.IsEmpty())
	{
		return E_INVALIDARG;
	}
	int pos = _path.ReverseFind(CHAR_PATH_SEPARATOR);
	if (pos < 0 || pos != _path.GetLength() - 1)
	{
		return E_FAIL;
	}
	CString parentPath = _path.Left(pos);
	pos = parentPath.ReverseFind(CHAR_PATH_SEPARATOR);
	if (pos < 0)
	{
		CFSDrives* drivesFolderSpec = new CComObject<CFSDrives>;
		drivesFolderSpec->FinalConstruct();
		CComPtr<IFolderFolder> drivesFolder = drivesFolderSpec;
		drivesFolderSpec->Init();
		drivesFolderSpec->FinalConstruct();
		*resultFolder = drivesFolder.Detach();
		return S_OK;
	}
	CString parentPathReduced = parentPath.Left(pos);
	parentPath = parentPath.Left(pos + 1);

	pos = parentPathReduced.ReverseFind(CHAR_PATH_SEPARATOR);
	if (pos == 1)
	{
		if (parentPath[0] != CHAR_PATH_SEPARATOR)
		{
			return E_FAIL;
		}
		Net::CNetFolder* netFolderSpec = new CComObject<Net::CNetFolder>;
		netFolderSpec->FinalConstruct();
		CComPtr<IFolderFolder> netFolder = netFolderSpec;
		netFolderSpec->Init(parentPath);
		*resultFolder = netFolder.Detach();
		return S_OK;
	}

	CFSFolder* parentFolderSpec = new CComObject<CFSFolder>;
	parentFolderSpec->FinalConstruct();
	CComPtr<IFolderFolder> parentFolder = parentFolderSpec;
	HRESULT hRes(parentFolderSpec->Init(parentPath, 0));
	if(S_OK != hRes)
	{
		return hRes;
	}
	*resultFolder = parentFolder.Detach();
	return S_OK;
}

STDMETHODIMP CFSFolder::GetNumberOfProperties(UINT32* numProperties)
{
	*numProperties = sizeof(kProps) / sizeof(kProps[0]);
	if (!_flatMode)
	{
		(*numProperties)--;
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::GetPropertyInfo (UINT32 index, BSTR* name, PROPID* propID, VARTYPE* varType)
{
	if(index >= sizeof(kProps) / sizeof(kProps[0]))
	{
		return E_INVALIDARG;
	}
	const STATPROPSTG &srcItem = kProps[index];
	*propID = srcItem.propid;
	*varType = srcItem.vt;
	*name = 0;
	return S_OK;
}

STDMETHODIMP CFSFolder::GetFolderProperty(PROPID propID, PROPVARIANT* value)
{
	try
	{
		CComPropVariant prop;
		switch(propID)
		{
			case kpidType:
				prop = CComBSTR(_T("FSFolder"));
				break;
			case kpidPath:
				prop = CComBSTR(_path);
				break;
		}
		prop.Detach(value);
		return S_OK;
	}
	catch(...)
	{
		return E_OUTOFMEMORY;
	}
}

STDMETHODIMP CFSFolder::WasChanged(INT32* wasChanged)
{
	bool wasChangedMain = false;
	for (;;)
	{
		if (!_findChangeNotification.IsHandleAllocated())
		{
			*wasChanged = BoolToInt(false);
			return S_OK;
		}

		DWORD waitResult = ::WaitForSingleObject(_findChangeNotification, 0);
		bool wasChangedLoc = (waitResult == WAIT_OBJECT_0);
		if (wasChangedLoc)
		{
			_findChangeNotification.FindNext();
			wasChangedMain = true;
		}
		else
		{
			break;
		}
	}
	*wasChanged = BoolToInt(wasChangedMain);
	return S_OK;
}

STDMETHODIMP CFSFolder::Clone(IFolderFolder** resultFolder)
{
	CFSFolder* fsFolderSpec = new CComObject<CFSFolder>;
	fsFolderSpec->FinalConstruct();
	CComPtr<IFolderFolder> folderNew = fsFolderSpec;
	fsFolderSpec->Init(_path, 0);
	*resultFolder = folderNew.Detach();
	return S_OK;
}

HRESULT CFSFolder::GetItemsFullSize(const UINT32* indices, UINT32 numItems,
									UINT64 &numFolders, UINT64 &numFiles, UINT64 &size, IProgress* progress)
{
	numFiles = numFolders = size = 0;
	UINT32 i;
	HRESULT hRes (S_OK);
	for (i = 0; i < numItems; i++)
	{
		size_t index = indices[i];
		if (index >= _refs.size())
		{
			return E_INVALIDARG;
		}
		const CDirItem &fi = *_refs[index];
		if (fi.IsDir())
		{
			UINT64 subFolders, subFiles, subSize;
			hRes = GetFolderSize(_path + GetRelPath(fi), subFolders, subFiles, subSize, progress);
			if(S_OK != hRes)
			{
				return hRes;
			}
			numFolders += subFolders;
			numFolders++;
			numFiles += subFiles;
			size += subSize;
		}
		else
		{
			numFiles++;
			size += fi.Size;
		}
	}
	return S_OK;
}

HRESULT CFSFolder::GetItemFullSize(int index, UINT64 &size, IProgress* progress)
{
	const CDirItem &fi = *_refs[index];
	if (fi.IsDir())
	{
		/*
		CComPtr<IFolderFolder> subFolder;
		RINOK(BindToFolder(index, &subFolder));
		CComPtr<IFolderReload> aFolderReload;
		subFolder.QueryInterface(&aFolderReload);
		aFolderReload->Reload();
		UINT32 numItems;
		RINOK(subFolder->GetNumberOfItems(&numItems));
		CComPtr<IFolderGetItemFullSize> aGetItemFullSize;
		subFolder.QueryInterface(&aGetItemFullSize);
		for (UINT32 i = 0; i < numItems; i++)
		{
		  UINT64 size;
		  RINOK(aGetItemFullSize->GetItemFullSize(i, &size));
		  *totalSize += size;
		}
		*/
		UINT64 numFolders, numFiles;
		return GetFolderSize(_path + GetRelPath(fi), numFolders, numFiles, size, progress);
	}
	size = fi.Size;
	return S_OK;
}

STDMETHODIMP CFSFolder::GetItemFullSize(UINT32 index, PROPVARIANT* value, IProgress* progress)
{
	CComPropVariant prop;
	if (index >= (UINT32)_refs.size())
	{
		return E_INVALIDARG;
	}
	UINT64 size = 0;
	HRESULT result = GetItemFullSize(index, size, progress);
	prop = size;
	prop.Detach(value);
	return result;
}

HRESULT CFSFolder::GetComplexName(const wchar_t* name, CString &resultPath)
{
	CString newName = name;
	resultPath = _path + newName;
	if (newName.GetLength() < 1)
	{
		return S_OK;
	}
	if (newName[0] == CHAR_PATH_SEPARATOR)
	{
		resultPath = newName;
		return S_OK;
	}
	if (newName.GetLength() < 2)
	{
		return S_OK;
	}
	if (newName[1] == _T(':'))
	{
		resultPath = newName;
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::CreateFolder(const wchar_t* name, IProgress* /* progress */)
{
	CString processedName;
	HRESULT hRes(GetComplexName(name, processedName));
	if(S_OK != hRes)
	{
		return hRes;
	}
	if(FileSystem::CreateDirectory(processedName))
	{
		return S_OK;
	}
	if(::GetLastError() == ERROR_ALREADY_EXISTS)
	{
		return ::GetLastError();
	}
	if (!FileSystem::CreateComplexDirectory(processedName))
	{
		return ::GetLastError();
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::CreateFile(const wchar_t* name, IProgress* /* progress */)
{
	CString processedName;
	HRESULT hRes(GetComplexName(name, processedName));
	if(S_OK != hRes)
	{
		return hRes;
	}
	Common::IO::COutFile outFile;
	if (!outFile.Create(processedName, false))
	{
		return ::GetLastError();
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::Rename(UINT32 index, const wchar_t* newName, IProgress* /* progress */)
{
	const CDirItem &fi = *_refs[index];
	const CString fullPrefix = _path + GetPrefix(fi);
	if (!FileSystem::MoveFile(fullPrefix + fi.Name, fullPrefix + newName))
	{
		return GetLastError();
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::Delete(const UINT32* indices, UINT32 numItems, IProgress* progress)
{
	HRESULT hRes(progress->SetTotal(numItems));
	if(S_OK != hRes)
	{
		return hRes;
	}
	for (UINT32 i = 0; i < numItems; i++)
	{
		const CDirItem &fi = *_refs[indices[i]];
		const CString fullPath = _path + GetRelPath(fi);
		bool result;
		if (fi.IsDir())
		{
			result = FileSystem::RemoveDirectoryWithSubItems(fullPath);
		}
		else
		{
			result = FileSystem::DeleteFileAlways(fullPath);
		}
		if (!result)
		{
			return GetLastError();
		}
		UINT64 completed = i;
		hRes = progress->SetCompleted(&completed);
		if(S_OK != hRes)
		{
			return hRes;
		}
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::SetProperty(UINT32 index, PROPID propID,
									const PROPVARIANT* value, IProgress* /* progress */)
{
	if (index >= (UINT32)_refs.size())
	{
		return E_INVALIDARG;
	}
	CDirItem &fi = *_refs[index];
	if (fi.Parent->Parent != 0)
	{
		return E_NOTIMPL;
	}
	switch(propID)
	{
		case kpidComment:
		{
			CString filename = fi.Name;
			filename.Trim();
			if (value->vt == VT_EMPTY)
			{
				_comments.DeletePair(filename);
			}
			else if (value->vt == VT_BSTR)
			{
				String::CTextPair pair;
				pair.ID = filename;
				pair.ID.Trim();
				pair.Value = value->bstrVal;
				pair.Value.Trim();
				if (pair.Value.IsEmpty())
				{
					_comments.DeletePair(filename);
				}
				else
				{
					_comments.AddPair(pair);
				}
			}
			else
			{
				return E_INVALIDARG;
			}
			SaveComments();
			break;
		}
		default:
			return E_NOTIMPL;
	}
	return S_OK;
}

STDMETHODIMP CFSFolder::GetSystemIconIndex(UINT32 index, INT32* iconIndex)
{
	if (index >= (UINT32)_refs.size())
	{
		return E_INVALIDARG;
	}
	const CDirItem &fi = *_refs[index];
	*iconIndex = 0;
	int iconIndexTemp;
	if (GetRealIconIndex(_path + GetRelPath(fi), fi.Attributes, iconIndexTemp) != 0)
	{
		*iconIndex = iconIndexTemp;
		return S_OK;
	}
	return GetLastError();
}

STDMETHODIMP CFSFolder::SetFlatMode(INT32 flatMode)
{
	_flatMode = IntToBool(flatMode);
	return S_OK;
}

}

}
}